import tkinter as tk
import time
import logging, logg

# Useful third party website
# https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/index.html
# https://tkdocs.com/tutorial/widgets.html 
# https://www.tutorialspoint.com/python3/python_gui_programming.htm 

# note format:
    # resource name: 
    # note:
    # resource value:

# current file logger
cf_logger = logging.getLogger('Tk_widgets_sample.advanced_widgets')

class ToplevelCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init toplevel
        self.toplevel = tk.Toplevel(self.master)
        self.toplevel['width'] = 200
        self.toplevel['height'] = 100

        self.toplevel.pack()

    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.toplevel.config(bg='red'), self.toplevel.config(background='red'),
        # configure -> self.toplevel.configure(bg='red'), self.toplevel.configure(background='red'),
        # self.toplevel['bg'] = 'red', self.toplevel['background'] = 'red'

        # Valid resource names: 
        # background, bd, bg, borderwidth, class,
        # colormap, container, cursor, height, highlightbackground,
        # highlightcolor, highlightthickness, menu, relief, screen, takefocus,
        # use, visual, width.

        self.toplevel[key] = value

    def resource_set_test(self):
        """
        Set the widget resource name with different value
        
        Try menu (could not find the usage of 'screen' & 'use')

        Other resources refer to basic_widgets.py
        """
        # resource name: menu
        # note: menu of toplevel window
        # resource value: menu object - details refer to advanced_components.py
        
        # top level menu
        menubar = tk.Menu(self.toplevel)
        self.resource_set('menu', menubar)

        # top level menu click command
        menubar.add_command(label='Topmenu_click', command=self.menu_callback)

        # top level cascade
        cascade_menu = tk.Menu(menubar, tearoff=False)
        cascade_menu.add_command(label='cascade option 1st', command=self.menu_callback)
        cascade_menu.add_command(label='cascade option 2ndt', command=self.menu_callback)

        menubar.add_cascade(label='cascade_name', menu=cascade_menu)

        # drop list menu cascade
        droplist_cascade_menu = tk.Menu(cascade_menu, tearoff=False)
        droplist_cascade_menu.add_command(label='droplist cascade option 1st', command=self.menu_callback)
        droplist_cascade_menu.add_command(label='droplist cascade option 2nd', command=self.menu_callback)

        cascade_menu.add_cascade(label='droplist_cascade_name', menu=droplist_cascade_menu)

        # log
        cf_logger.info('ToplevelCustom object set menu.')

        # run mainloop
        self.run()

    def run(self):
        # run master mainloop
        self.master.mainloop()

    def menu_callback(self):
        cf_logger.info('click meun.')

    def methods_toplevel_test(self):
        """
        Try all common methods of Toplevel object.
        Not really run below methods, just try to understand it by doc.

        .aspect & .wm_aspect are the same, others apply to both usages  
        """
        # Constrain the root window's width:length ratio to the range [ nmin / dmin, nmax / dmax ].
        self.toplevel.aspect(1,2,1,1)

        # show the window
        self.toplevel.deiconify()

        # hide the window ( minimize )
        self.toplevel.iconify()

        # Hides the window. Restore it with .deiconify() or .iconify().
        self.toplevel.withdraw()

        # Set the window geometry
        self.toplevel.geometry('200x100')

        # To raise this window to the top of the stacking order in the window manager, 
        # call this method with no arguments. You can also raise it to a position in the 
        # stacking order just above another Toplevel window by passing that window as an 
        # argument.
        self.toplevel.lift()

        # If the argument is omitted, moves the window to the bottom of the stacking order 
        # in the window manager. You can also move the window to a position just under some 
        # other top-level window by passing that Toplevel widget as an argument.
        self.toplevel.lower()

        # Set the maximum window size. If the arguments are omitted, returns the current (width, height).
        self.toplevel.maxsize(width=500, height=400)
        
        # Set the minimum window size. If the arguments are omitted, returns the current minima as a 2-tuple.
        self.toplevel.minsize(width=1, height=1)

        # If called with a True argument, this method sets the override redirect flag, which removes all window 
        # manager decorations from the window, so that it cannot be moved, resized, iconified, or closed. 
        # If called with a False argument, window manager decorations are restored and the override redirect flag 
        # is cleared. If called with no argument, it returns the current state of the override redirect flag.
        # Be sure to call the .update_idletasks() method (see Section 26, “Universal widget methods”) before setting 
        # this flag. If you call it before entering the main loop, your window will be disabled before it ever appears.
        # This method may not work on some Unix and MacOS platforms.
        self.toplevel.overrideredirect(False)

        # If is true, allow horizontal resizing. If height is true, allow vertical resizing. If the arguments are 
        # omitted, returns the current size as a 2-tuple.
        self.toplevel.resizable(True, True)

        # Returns the window's current state, one of normal, iconic, withdrawn, or zoomed: 
        # 'normal': Displayed normally.
        # 'iconic': Iconified with the .iconify() method.
        # 'withdrawn': Hidden; see the .withdraw() method below.
        # 'zoomed' : Zoom
        # To change the window's state, pass one of the strings above as an argument to the method. 
        # For example, to iconify a Toplevel instance T, use “T.state('iconic') ”.
        self.toplevel.state('zoomed')

        # Set the window title. If the argument is omitted, returns the current title.
        self.toplevel.title('Toplevel title')

        # Make this window a transient window for some parent window; the default parent window is this window's parent.
        # This method is useful for short-lived pop-up dialog windows. A transient window always appears in front of its
        # parent. If the parent window is iconified, the transient is iconified as well.
        self.toplevel.transient(self.master) 


class PanedWindowCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init panedwindow
        self.panedwindow = tk.PanedWindow(self.master)
        self.panedwindow['width'] = 200
        self.panedwindow['height'] = 100

        self.panedwindow.pack()

    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.panedwindow.config(bg='red'), self.panedwindow.config(background='red'),
        # configure -> self.panedwindow.configure(bg='red'), self.panedwindow.configure(background='red'),
        # self.panedwindow['bg'] = 'red', self.panedwindow['background'] = 'red'

        # STANDARD OPTIONS

        #    background, borderwidth, cursor, height,
        #    orient, relief, width

        # WIDGET-SPECIFIC OPTIONS

        #    handlepad, handlesize, opaqueresize,
        #    sashcursor, sashpad, sashrelief,
        #    sashwidth, showhandle,

        self.panedwindow[key] = value

    def resource_set_test(self):
        """
        Set the widget resource name with different value
        
        Try orient, showhandle, handlepad, handlesize, opaqueresize, sashpad

        Other resources refer to basic_widgets.py
        and refer to https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/panedwindow.html 
        """
        # resource name: orient
        # note: To stack child widgets side by side, use orient=tk.HORIZONTAL. To stack them top to bottom, use orient=tk.VERTICAL.
        # resource value: tk.HORIZONTAL, tk.VERTICAL
        self.resource_set('orient', tk.HORIZONTAL)
        cf_logger.info('PanedWindowCustom object set orient.')

        # resource name: showhandle
        # note: Use showhandle=True to display the handles. For the default value, False, the user can still use the 
        # mouse to move the sashes. The handle is simply a visual cue.
        # resource value: True, False
        self.resource_set('showhandle', True)
        cf_logger.info('PanedWindowCustom object set showhandle.')

        # resource name: handlepad
        # note: Use this option to specify the distance between the handle and the end of the sash. For orient=tk.VERTICAL,
        # this is the distance between the left end of the sash and the handle; for orient=tk.HORIZONTAL, it is the distance 
        # between the top of the sash and the handle. The default value is eight pixels;
        # resource value: int
        self.resource_set('handlepad', 10)
        cf_logger.info('PanedWindowCustom object set handlepad.')

        # resource name: handlesize
        # note: Use this option to specify the size of the handle, which is always a square; The default value is eight pixels.
        # resource value: int
        self.resource_set('handlesize', 10)
        cf_logger.info('PanedWindowCustom object set handlesize.')

        # resource name: opaqueresize
        # note: This option controls how a resizing operation works. For the default value, opaqueresize=True, the resizing is 
        # done continuously as the sash is dragged. If this option is set to False, the sash (and adjacent child widgets) stays
        # put until the user releases the mouse button, and then it jumps to the new position.
        # resource value: True, False
        self.resource_set('opaqueresize', True)
        cf_logger.info('PanedWindowCustom object set opaqueresize.')

        # resource name: sashpad
        # note: Use this option to allocate extra space on either side of each sash. The default is zero; for other values.
        # resource value: int
        self.resource_set('sashpad', 20)
        cf_logger.info('PanedWindowCustom object set sashpad.')

        # run mainloop
        self.run()

    def run(self):
        # run master mainloop
        self.master.mainloop()

    def methods_panedwindow_test(self):
        """
        Try all common methods of PanedWindow object.
        Not really run below methods, just try to understand it by doc.
        """
        # Use this method to add the given child widget as the next child of this PanedWindow. First create the child widget with 
        # the PanedWindow as its parent widget, but do not call the .grid() method to register it. Then call .add(child) and the 
        # child will appear inside the PanedWindow in the next available position.
        # Associated with each child is a set of configuration options that control its position and appearance. You can supply 
        # these configuration options as keyword arguments to the .add() method. You can also set or change their values anytime 
        # with the .paneconfig() method, or retrieve the current value of any of these options using the .panecget() method; these 
        # methods are described below.
        label = tk.Label(self.panedwindow, text='paned label')
        self.panedwindow.add(label)

        # Removes a child widget.
        self.panedwindow.forget(label) # same with .remove() method

        # For a given location (x, y) in window coordinates, this method returns a value that describes the feature at that location.
        # If the feature is a child window, the method returns an empty string.
        # If the feature is a sash, the method returns a tuple (n, 'sash') where n is 0 for the first sash, 1 for the second, and so on.
        # If the feature is a handle, the method returns a tuple (n, 'handle') where n is 0 for the first handle, 1 for the second, and so on.
        location_fea = self.panedwindow.location(0, 10)
        cf_logger.info('Panedwindow object location feature: {}'.format(location_fea))

        # Use this method to configure options for child widgets. The options are described in 
        # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/pane-options.html
        self.panedwindow.add(label) # removed above, add it back
        self.panedwindow.paneconfig(label, height=10)

        # This method returns a list of the child widgets, in order from left to right (for orient=tk.HORIZONTAL) or 
        # top to bottom (for orient=tk.VERTICAL).
        child_list = self.panedwindow.panes()
        cf_logger.info('Panedwindow object child_list: {}'.format(child_list))

        # This method retrieves the value of a child widget configuration option, where child is the child widget and option is the name 
        # of the option as a string. For the list of child widget configuration options
        value = self.panedwindow.panecget(label, 'height')
        cf_logger.info('Panedwindow object value of height by panecget method: {}'.format(value))

        # This method returns the location of a sash. The index argument selects the sash: 0 for the sash between the first two children, 
        # 1 for the sash between the second and third child, and so forth. The result is a tuple (x, y) containing the coordinates of the 
        # upper left corner of the sash.
        coords = self.panedwindow.sash_coord(0)
        cf_logger.info('Panedwindow object value of sash by sash_coord method: {}'.format(coords))

        # Use this method to reposition the sash selected by index (0 for the first sash, and so on). The x and y coordinates specify 
        # the desired new position of the upper left corner of the sash. Tkinter ignores the coordinate orthogonal to the orientation 
        # of the widget: use the x value to reposition the sash for orient=tk.HORIZONTAL, and use the y coordinate to move the sash for 
        # option orient=tk.VERTICAL.
        self.panedwindow.sash_place(0, 20, 10)


class ListboxCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init listbox
        self.listbox = tk.Listbox(self.master)
        self.listbox['width'] = 5
        self.listbox['height'] = 8

        self.listbox.insert(1, 'option 1')
        self.listbox.insert(2, 'option 2')
        self.listbox.insert(3, 'option 3')

        self.listbox.pack()

    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.listbox.config(bg='red'), self.listbox.config(background='red'),
        # configure -> self.listbox.configure(bg='red'), self.listbox.configure(background='red'),
        # self.listbox['bg'] = 'red', self.listbox['background'] = 'red'

        # Valid resource names: 
        # background, bd, bg, borderwidth, cursor, activestyle
        # exportselection, fg, font, foreground, height, highlightbackground,
        # highlightcolor, highlightthickness, relief, selectbackground,
        # selectborderwidth, selectforeground, selectmode, setgrid, takefocus,
        # width, xscrollcommand, yscrollcommand, listvariable.

        self.listbox[key] = value

    def resource_set_test(self):
        """
        Set the widget resource name with different value
        Try activestyle, exportselection, selectmode, setgrid, xscrollcommand, yscrollcommand, listvariable

        Other resources refer to basic_widgets.py
        """
        # resource name: activestyle
        # note: the appearance of the active line
        # resource value: 'underline', 'dotbox', 'none'
        self.resource_set('activestyle', 'dotbox')
        cf_logger.info('LixtboxCustom object set activestyle.')

        # resource name: exportselection
        # note: By default, the user may select text with the mouse, and the selected text will be exported to 
        # the clipboard. To disable this behavior, use exportselection=0.
        # resource value: 0
        self.resource_set('exportselection', 0)
        cf_logger.info('LixtboxCustom object set exportselection.')

        # resource name: selectmode
        # note: Determines how many items can be selected, and how mouse drags affect the selection:
        # tk.BROWSE: Normally, you can only select one line out of a listbox. If you click on an item and then 
        # drag to a different line, the selection will follow the mouse. This is the default.

        # tk.SINGLE: You can only select one line, and you can't drag the mouse—wherever you click button 1, that line is selected.

        # tk.MULTIPLE: You can select any number of lines at once. Clicking on any line toggles whether or not it is selected.

        # tk.EXTENDED: You can select any adjacent group of lines at once by clicking on the first line and dragging to the last line.

        # resource value: refer to above
        self.resource_set('selectmode', tk.BROWSE)
        cf_logger.info('LixtboxCustom object set selectmode.')
        

        # resource name: setgrid
        # note: Determines whether to enable grid control. Default is False.
        # resource value: True or False
        self.resource_set('setgrid', True)
        cf_logger.info('LixtboxCustom object set setgrid.')

        # resource name:  xscrollcommand, yscrollcommand,
        # note: scrollbar of the listbox, refer to https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/listbox-scrolling.html 
        # resource value: scrollbar object
        xs = tk.Scrollbar(self.master, orient=tk.HORIZONTAL)
        ys = tk.Scrollbar(self.master, orient=tk.VERTICAL)
        xs.pack(side=tk.TOP, fill='both')
        ys.pack(side=tk.RIGHT, fill='both')

        #config them
        xs.config(command=self.listbox.xview)
        ys.config(command=self.listbox.yview)
        self.listbox.config(xscrollcommand=xs.set, yscrollcommand=ys.set)

        cf_logger.info('LixtboxCustom object set xscrollcommand.')
        cf_logger.info('LixtboxCustom object set yscrollcommand.')

        # resource name: listvariable
        # note: A StringVar that is connected to the complete list of values in the listbox.
        # If you call the .get() method of the listvariable, you will get back a string of the form "('v0', 'v1', ...)", 
        # where each vi is the contents of one line of the listbox. To change the entire set of lines in the listbox at once, 
        # call .set(s) on the listvariable, where s is a string containing the line values with spaces between them.

        # For example, if listCon is a StringVar associated with a listbox's listvariable option, this call would set the listbox to contain three lines:

        # listCon.set('ant bee cicada')
        # This call would return the string "('ant', 'bee', 'cicada')":

        # listCon.get()

        # resource value: StringVar object
        svar = StringVar()
        self.resource_set('listvariable', svar)
        cf_logger.info('LixtboxCustom object set listvariable.')

        # run mainloop
        self.run()

    def run(self):
        # run master mainloop
        self.master.mainloop()

    def methods_listbox_test(self):
        """
        Try all common methods of Listbox object.
        Not really run below methods, just try to understand it by doc.
        """
        # note:
        # A special set of index forms is used for many of the methods on listbox objects:

        # If you specify an index as an integer, it refers to the line in the listbox with that index, counting from 0.

        # Index tk.END refers to the last line in the listbox.

        # Index tk.ACTIVE refers to the selected line. If the listbox allows multiple selections, it refers to the line that was last selected.

        # An index string of the form '@x,y' refers to the line closest to coordinate (x,y) relative to the widget's upper left corner.

        # Selects the line specifies by the given index.
        self.listbox.activate(1)

        # Returns the bounding box of the line specified by index as a 4-tuple (xoffset, yoffset, width, height), where the upper left pixel 
        # of the box is at (xoffset, yoffset) and the width and height are given in pixels. The returned width value includes only the part of 
        # the line occupied by text. If the line specified by the index argument is not visible, this method returns None. If it is partially 
        # visible, the returned bounding box may extend outside the visible area.
        bb = self.listbox.bbox(1)
        cf_logger.info('LixtboxCustom object bounding box {}.'.format(bb))
        
        # Returns a tuple containing the line numbers of the selected element or elements, counting from 0. If nothing is selected, returns
        # an empty tuple.
        selected_content = self.listbox.curselection()
        cf_logger.info('LixtboxCustom object selected content {}.'.format(selected_content))

        # Deletes the lines whose indices are in the range [first, last], inclusive (contrary to the usual Python idiom, where deletion stops 
        # short of the last index), counting from 0. If the second argument is omitted, the single line with index first is deleted.
         self.listbox.delete(1)

        # Returns a tuple containing the text of the lines with indices from first to last, inclusive. If the second argument is omitted, returns 
        # the text of the line closest to first.
        self.listbox.get(1)

        # If possible, positions the visible part of the listbox so that the line containing index i is at the top of the widget.
        self.listbox.index(2)

        # Insert one or more new lines into the listbox before the line specified by index. Use tk.END as the first argument if you want to add 
        # new lines to the end of the listbox.
        self.listbox.insert(tk.END, 'option 4')

        # Change a configuration option for the line specified by index. 
        # Option names include: background, foreground, selectbackground, selectforeground
        self.listbox.itemconfig(1, background='red')

        # Retrieves one of the option values for a specific line in the listbox. For option values, see itemconfig below. If the given option has 
        # not been set for the given line, the returned value will be an empty string.
        self.listbox.itemcget(1, 'background')

        # Return the index of the visible line closest to the y-coordinate y relative to the listbox widget.
        nearest_index = self.listbox.nearest(5)
        cf_logger.info('LixtboxCustom object nearest index {}.'.format(nearest_index))

        # Use this method to implement scanning—fast steady scrolling—of a listbox. To get this feature, bind some mouse button event to a handler 
        # that calls scan_mark with the current mouse position. Then bind the <Motion> event to a handler that calls scan_dragto with the current 
        # mouse position, and the listbox will be scrolled at a rate proportional to the distance between the position recorded by scan_mark and the
        # current position.

        # .scan_mark(x, y), .scan_dragto(x, y), refer to same methods of Entry object in basic_widgets.py.

        # Adjust the position of the listbox so that the line referred to by index is visible.
        self.listbox.see(1)

        # Place the “selection anchor” on the line selected by the index argument. Once this anchor has been placed, you can refer to it with the 
        # special index form tk.ANCHOR.
        # For example, for a listbox named lbox, this sequence would select lines 3, 4, and 5:
        # lbox.selection_anchor(3)
        # lbox.selection_set(tk.ANCHOR,5)
        self.listbox.selection_anchor(2)

        # Unselects all of the lines between indices first and last, inclusive. If the second argument is omitted, unselects the line with index first.
        self.listbox.selection_clear(1)

        # Returns 1 if the line with the given index is selected, else returns 0.
        selected_flag = self.listbox.selection_includes(1)
        cf_logger.info('LixtboxCustom object selected flag {}.'.format(selected_flag))

        # Selects all of the lines between indices first and last, inclusive. If the second argument is omitted, selects the line with index first.
        self.listbox.selection_set(1)

        # Returns the number of lines in the listbox.
        lines_number = self.listbox.size()
        cf_logger.info('LixtboxCustom object lines number {}.'.format(lines_number))

        # To make the listbox horizontally scrollable, set the command option of the associated horizontal scrollbar to this method. 
        # To make the listbox vertically scrollable, set the command option of the associated vertical scrollbar to this method.
        # .xview(), .yview() - refer to above xscrollcommand, yscrollcommand option usage. 
        # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/listbox-scrolling.html

        #--------------------------------- sep ------------------------------------

        # Scroll the listbox so that the leftmost fraction of the width of its longest line is outside the left side of the 
        # listbox. Fraction is in the range [0,1].

        # Scrolls the listbox horizontally. For the what argument, use either tk.UNITS to scroll by characters, or tk.PAGES to 
        # scroll by pages, that is, by the width of the listbox. The number argument tells how many to scroll; negative values 
        # move the text to the right within the listbox, positive values leftward.

        # Scroll the listbox so that the top fraction of the width of its longest line is outside the left side of the listbox.
        # Fraction is in the range [0,1].

        # Scrolls the listbox vertically. For the what argument, use either tk.UNITS to scroll by lines, or tk.PAGES to scroll 
        # by pages, that is, by the height of the listbox. The number argument tells how many to scroll; negative values move the 
        # text downward inside the listbox, and positive values move the text up.

        # .xview_moveto(fraction), .xview_scroll(number, what), .yview_moveto(fraction), .yview_scroll(number, what)
        # refer to Entry object in basic_widgets.py file, there are same methods.


class SpinboxCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init spinbox
        self.spinbox = tk.Spinbox(self.master)
        self.spinbox.config(width=10, from_=0, to=20)

        self.spinbox.pack()

    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.spinbox.config(bg='red'), self.spinbox.config(background='red'),
        # configure -> self.spinbox.configure(bg='red'), self.spinbox.configure(background='red'),
        # self.spinbox['bg'] = 'red', self.spinbox['background'] = 'red'

        # STANDARD OPTIONS

        #    activebackground, background, borderwidth,
        #    cursor, exportselection, font, foreground,
        #    highlightbackground, highlightcolor,
        #    highlightthickness, insertbackground,
        #    insertborderwidth, insertofftime,
        #    insertontime, insertwidth, justify, relief,
        #    repeatdelay, repeatinterval,
        #    selectbackground, selectborderwidth
        #    selectforeground, takefocus, textvariable
        #    xscrollcommand.

        # WIDGET-SPECIFIC OPTIONS

        #    buttonbackground, buttoncursor,   
        #    buttondownrelief, buttonuprelief,
        #    command, disabledbackground,
        #    disabledforeground, format, from_,
        #    invalidcommand, increment,
        #    readonlybackground, state, to,
        #    validate, validatecommand, values,
        #    width, wrap,

        self.spinbox[key] = value

    def resource_set_test(self):
        """
        Set the widget resource name with different value
        Try buttonbackground, buttoncursor, buttondownrelief, buttonuprelief, 
        invalidcommand, validate, validatecommand, 
        from_, to, increment, 
        state, command, format, exportselection
        values, width, wrap

        Other resources refer to basic_widgets.py
        """
        # buttonbackground, buttoncursor, buttondownrelief, buttonuprelief
        # bg, cursor, relief of the button, the button means the arrowheads by rightside of spinbox

        # invalidcommand, validate, validatecommand, 
        # refer to Entry object methods in basic_widgets.py file.

        # resource name: from_, to, increment, 
        # note: min, max value of the spinbox, the increment is the step, 
        # like from_=0, to=20, increment=5, it will change 0, 5, 10, 15, 20
        # resource value: int
        self.resource_set('from_', 0)
        self.resource_set('to', 20)
        self.resource_set('increment', 5)
        cf_logger.info('SpinboxCustom object set from_.')
        cf_logger.info('SpinboxCustom object set to.')
        cf_logger.info('SpinboxCustom object set increment.')

        # resource name: state
        # note: state of spinbox
        # resource value: tk.NORMAL, tk.DISABLED, 'readonly'
        self.resource_set('state', tk.NORMAL)
        cf_logger.info('SpinboxCustom object set from_.')

        # resource name: command
        # note: Use this option to specify a function or method to be called whenever the user clicks on 
        # one of the arrowheads. Note that the callback is not called when the user edits the value 
        # directly as if it were an Entry.
        # resource value: function or method
        self.resource_set('command', self.spinbox_callback)
        cf_logger.info('SpinboxCustom object set command.')
        
        # resource name: format
        # note: Use this option to control the formatting of numeric values in combination with the from_ 
        # and to options. For example, format='%10.4f' would display the value as a ten-character field, 
        # with four digits after the decimal.
        # resource value: format string
        self.resource_set('format', '%8.5f')
        cf_logger.info('SpinboxCustom object set format.')

        # resource name: exportselection
        # note: Normally, the text in the entry portion of a Spinbox can be cut and pasted. To prohibit
        # this behavior, set the exportselection option to True.
        # resource value: True
        self.resource_set('exportselection', True)
        cf_logger.info('SpinboxCustom object set exportselection.')

        # resource name: values
        # note: There are two ways to specify the possible values of the widget. One way is to provide a tuple 
        # of strings as the value of the values option. For example, values=('red', 'blue', 'green') would allow 
        # only those three strings as values. To configure the widget to accept a range of numeric values, see 
        # the from_ option above.
        # resource value: tuple of strings
        self.resource_set('values', ('red', 'blue', 'green'))
        cf_logger.info('SpinboxCustom object set values.')

        # resource name: width
        # note: Use this option to specify the number of characters allowed in the entry part of the widget. 
        # The default value is 20.
        # resource value: int
        self.resource_set('width', 15)
        cf_logger.info('SpinboxCustom object set width.')

        # resource name: state
        # note: Normally, when the widget is at its highest value, the up-arrowhead does nothing, and when the 
        # widget is at its lowest value, the down-arrowhead does nothing. If you select wrap=True, the up-arrowhead 
        # will advance from the highest value back to the lowest, and the down-arrowhead will advance from the 
        # lowest value back to the highest.
        # resource value: True
        self.resource_set('wrap', True)
        cf_logger.info('SpinboxCustom object set wrap.')

        # run mainloop
        self.run()

    def run(self):
        # run master mainloop
        self.master.mainloop()

    def spinbox_callback(self):
        cf_logger.info('SpinboxCustom callback.')

    def methods_spinbox_test(self):
        """
        Try all common methods of Spinbox object.
        Not really run below methods, just try to understand it by doc.
        """
        # .bbox(index), .delete(first, last=None), .get(), .icursor(index), .identify(x, y), .index(i)
        # .insert(index, text), .invoke(element), .scan_dragto(x), .scan_mark(x)
        # .selection('from', index), .selection('to', index), .selection('range', start, end)
        # .selection_clear(), .selection_get()

        # refer to Entry object in basic_widgets.py file, and refer to listbox object above, all same methods
        # refer to https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/spinbox.html 


class TextCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init text
        self.text = tk.Text(self.master)
        self.text.config(width=50, height=20)

        self.text.pack()

    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.text.config(bg='red'), self.text.config(background='red'),
        # configure -> self.text.configure(bg='red'), self.text.configure(background='red'),
        # self.text['bg'] = 'red', self.text['background'] = 'red'

        # STANDARD OPTIONS

        #    background, borderwidth, cursor,
        #    exportselection, font, foreground,
        #    highlightbackground, highlightcolor,
        #    highlightthickness, insertbackground,
        #    insertborderwidth, insertofftime,
        #    insertontime, insertwidth, padx, pady,
        #    relief, selectbackground,
        #    selectborderwidth, selectforeground,
        #    setgrid, takefocus,
        #    xscrollcommand, yscrollcommand,

        # WIDGET-SPECIFIC OPTIONS

        #    autoseparators, height, maxundo,
        #    spacing1, spacing2, spacing3,
        #    state, tabs, undo, width, wrap,

        self.text[key] = value

    def resource_set_test(self):
        """
        Set the widget resource name with different value
        Try exportselection，undo, maxundo, autoseparators
        spacing1, spacing2, spacing3,
        tabs, wrap,

        Other resources refer to basic_widgets.py
        """
        # resource name: exportselection
        # note: Normally, text selected within a text widget is exported to be the selection in the window manager. 
        # Set exportselection=0 if you don't want that behavior.
        # resource value: 0
        self.resource_set('exportselection', 0)
        cf_logger.info('TextCustom object set exportselection.')

        # undo mechanism - https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/text-undo-stack.html 

        # resource name: undo
        # note: Set this option to True to enable the undo mechanism, or False to disable it. 
        # resource value: True or False
        self.resource_set('undo', True)
        cf_logger.info('TextCustom object set undo.')

        # resource name: maxundo
        # note: This option sets the maximum number of operations retained on the undo stack. For an overview of the 
        # undo mechanism, Set this option to -1 to specify an unlimited number of entries in the undo stack.
        # resource value: int
        self.resource_set('maxundo', 5)
        cf_logger.info('TextCustom object set maxundo.')

        # resource name: autoseparators
        # note: If the undo option is set, the autoseparators option controls whether separators are automatically added 
        # to the undo stack after each insertion or deletion (if autoseparators=True) or not (if autoseparators=False). 
        # resource value: True or False
        self.resource_set('autoseparators', True)
        cf_logger.info('TextCustom object set autoseparators.')

        # resource name: spacing1
        # note: This option specifies how much extra vertical space is put above each line of text. If a line wraps, 
        # this space is added only before the first line it occupies on the display. Default is 0.
        # resource value: int
        self.resource_set('spacing1', 3)
        cf_logger.info('TextCustom object set spacing1.')

        # resource name: spacing2
        # note: This option specifies how much extra vertical space to add between displayed lines of text when 
        # a logical line wraps. Default is 0.
        # resource value: int
        self.resource_set('spacing2', 4)
        cf_logger.info('TextCustom object set spacing2.')

        # resource name: spacing3
        # note: This option specifies how much extra vertical space is added below each line of text. If a line wraps, 
        # this space is added only after the last line it occupies on the display. Default is 0.
        # resource value: int
        self.resource_set('spacing3', 5)
        cf_logger.info('TextCustom object set spacing3.')

        # resource name: spacing3
        # note: This option specifies how much extra vertical space is added below each line of text. If a line wraps, 
        # this space is added only after the last line it occupies on the display. Default is 0.
        # resource value: int
        self.resource_set('spacing3', 5)
        cf_logger.info('TextCustom object set spacing3.')

        # resource name: tabs
        # note: This option controls how tab characters position text.  
        # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/text-tabs.html
        # resource value: tuple, refer to above link
        tabs_value=('0.5i', '0.8i', tk.RIGHT, '1.2i', tk.CENTER, '2i', tk.NUMERIC)
        self.resource_set('tabs', tabs_value)
        cf_logger.info('TextCustom object set tabs.')

        # resource name: wrap
        # note: This option controls the display of lines that are too wide.
        # With the default behavior, wrap=tk.CHAR, any line that gets too long will be broken at any character.
        # Set wrap=tk.WORD and it will break the line after the last word that will fit.
        # If you want to be able to create lines that are too long to fit in the window, set wrap=tk.NONE and provide a horizontal scrollbar.
        # resource value: tk.CHAR, tk.WORD, tk.NONE
        self.resource_set('wrap', tk.CHAR)
        cf_logger.info('TextCustom object set wrap.')

        # run mainloop
        self.run()

    def run(self):
        # run master mainloop
        self.master.mainloop()

    def methods_text_test(self):
        """
        Try all common methods of Text object.
        Not really run below methods, just try to understand it by doc.
        https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/text-methods.html 
        """
        # ---------------- get & compare --------------------------

        #Returns the bounding box for the character at the given index, a 4-tuple (x, y, width, height). If the character is not visible, 
        # returns None. Note that this method may not return an accurate value unless you call the .update_idletasks() method
        bbox_value = self.text.bbox(1)
        cf_logger.info('TextCustom object bbox value {}.'.format(bbox_value))

        # Compares the positions of two indices in the text widget, and returns true if the relational op holds between index1 and index2. 
        # The op specifies what comparison to use, one of: '<', '<=', '==', '!=', '>=', or '>'.
        # For example, for a text widget t, t.compare('2.0', '<=', END) returns true if the beginning of the second line is before or at
        # the end of the text in t.
        compare_flag = self.text.compare(1.0, '<', tk.END) # 1.0 means 1 line first characther
        cf_logger.info('TextCustom object compare_flag value {}.'.format(compare_flag))

        # Returns a bounding box for the line that contains the given index. For the form of the bounding box, and a caution about updating 
        # idle tasks, see the definition of the .bbox method above.
        bbox_value = self.text.dlineinfo(1)
        cf_logger.info('TextCustom object dlineinfo value {}.'.format(bbox_value))

        # Use this method to retrieve the current text from the widget. Retrieval starts at index index1. If the second argument is omitted, 
        # you get the character after index1. If you provide a second index, you get the text between those two indices. Embedded images 
        # and windows (widgets) are ignored. If the range includes multiple lines, they are separated by newline ('\n') characters.
        curent_text = self.text.get(1.0, tk.END)
        cf_logger.info('TextCustom object curent_text value {}.'.format(curent_text))

        # For an index i, this method returns the equivalent position in the form 'line.char'.
        characther_position = self.text.index(10)
        cf_logger.info('TextCustom object characther_position {}.'.format(characther_position))

        # ---------------- operation --------------------------

        # Inserts the given text at the given index.
        # If you omit the tags argument, the newly inserted text will be tagged with any tags that apply to the characters both before 
        # and after the insertion point.
        # If you want to apply one or more tags to the text you are inserting, provide as a third argument a tuple of tag strings. 
        # Any tags that apply to existing characters around the insertion point are ignored. Note: The third argument must be a tuple. 
        # If you supply a list argument, Tkinter will silently fail to apply the tags. If you supply a string, each character will be 
        # treated as a tag.
        self.text.insert(1.0, 'hellow world.')

        # Deletes text starting just after index1. If the second argument is omitted, only one character is deleted. If a second index is 
        # given, deletion proceeds up to, but not including, the character after index2. Recall that indices sit between characters.
        self.text.delete(1.0, 1.2)

        # .scan_dragto(x, y), .scan_mark(x, y) refer to Entry object in basic_widgets.py

        # If the text containing the given index is not visible, scroll the text until that text is visible.
        self.text.see(tk.END)

        # Searches for pattern (which can be either a string or a regular expression) in the buffer starting at the given index. 
        # If it succeeds, it returns an index of the 'line.char' form; if it fails, it returns an empty string.
        # The allowable options for this method are:
        # backwards--	Set this option to True to search backwards from the index. Default is forwards.
        # count	If you set this option to an IntVar control variable, when there is a match you can retrieve the length of the 
        # text that matched by using the .get() method on that variable after the method returns.

        # exact--	Set this option to True to search for text that exactly matches the pattern. This is the default option. 
        # Compare the regexp option below.

        # forwards--	Set this option to True to search forwards from the index. This is the default option.

        # regexp--	Set this option to True to interpret the pattern as a Tcl-style regular expression. 
        # The default is to look for an exact match to pattern. Tcl regular expressions are a subset of Python regular expressions, 
        # supporting these features: . ^ [c1…] (…) * + ? e1|e2

        # nocase--	Set this option to 1 to ignore case. The default is a case-sensitive search.

        # stopindex --	To limit the search, set this option to the index beyond which the search should not go.
        search_pos = self.text.search(' ', 1.0, nocase=True)
        cf_logger.info('TextCustom object search_pos value {}.'.format(search_pos))

        # ---------------- edit --------------------------

        # Queries, sets, or clears the modified flag. This flag is used to track whether the contents of the widget have been changed. 
        # For example, if you are implementing a text editor in a Text widget, you might use the modified flag to determine whether the 
        # contents have changed since you last saved the contents to a file.
        # When called with no argument, this method returns True if the modified flag has been set, False if it is clear. You can also 
        # explicitly set the modified flag by passing a True value to this method, or clear it by passing a False value.
        # Any operation that inserts or deletes text, whether by program actions or user actions, or an undo or redo operation, will set the modified flag.
        self.text.edit_modified(True) # or could get the flag by omitting the arg

        # Performs a redo operation.
        self.text.redo()

        # Clears the undo stack.
        self.text.edit_reset()

        # Pushes a separator onto the undo stack. This separator limits the scope of a future undo operation to include only the changes 
        # pushed since the separator was pushed.
        self.text.edit_separator()

        # Reverses all changes to the widget's contents made since the last separator was pushed on the undo stack, or all the way to the 
        # bottom of the stack if the stack contains no separators. For details
        self.text.edit_undo()

        # ---------------- image --------------------------

        # This method inserts an image into the widget. The image is treated as just another character, whose size is the image's natural size.
        # The options for this method are shown in the table below. You may pass either a series of option=value arguments, 
        # or a dictionary of option names and values.

        # align	--This option specifies how the image is to be aligned vertically if its height is less than the height of its containing line. 
        # Values may be top to align it at the top of its space; center to center it; bottom to place it at the bottom; or baseline to line 
        # up the bottom of the image with the text baseline.

        # image --The image to be used. Refer to https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/images.html 

        # name	--You can assign a name to this instance of the image. If you omit this option, Tkinter will generate a unique name. 
        # If you create multiple instances of an image in the same Text widget, Tkinter will generate a unique name by appending a “#” followed by a number.

        # padx --If supplied, this option is a number of pixels of extra space to be added on both sides of the image.
        # pady --If supplied, this option is a number of pixels of extra space to be added above and below the image.
        self.text.image_create(1.0, align=tk.TOP, image='', name='im') # image is the Image object

        # To retrieve the current value of an option set on an embedded image, call this method with an index pointing to the image and the name of the option.
        image_name = self.text.image_cget(1.1, 'name')
        cf_logger.info('TextCustom object image_name value {}.'.format(image_name))

        # To set one or more options on an embedded image, call this method with an index pointing to the image as the first argument, and one or more 
        # option=value pairs. If you specify no options, you will get back a dictionary defining all the options on the image, and the corresponding values.
        self.text.image_configure(1.1, name='image_name', padx=5, pady=5)

        # This method returns a tuple of the names of all the text widget's embedded images.
        image_name_tuple = self.text.image_names()
        cf_logger.info('TextCustom object image_name_tuple {}.'.format(image_name_tuple))

        # ---------------- mark --------------------------
        # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/text-mark.html 

        # If no mark with name mark exists, one is created with tk.RIGHT gravity and placed where index points. If the mark already exists, 
        # it is moved to the new location. This method may change the position of the tk.INSERT or tk.CURRENT indices.
        self.text.makr_set('mark#1', 1.2)
        self.text.makr_set('mark#2', 2.2)

        # Removes the named mark. This method cannot be used to remove the tk.INSERT or tk.CURRENT marks.
        self.text.mark_unset('mark#2')

        # Returns the name of the mark preceding the given index. If there are no preceding marks, the method returns an empty string.
        # If the index is in numeric form, the method returns returns the last mark at that position. If the index is a mark, the method 
        # returns the preceding mark, which may be at the same numerical position.
        previous_marks = self.text.mark_previous(1.3)
        cf_logger.info('TextCustom object previous_marks {}.'.format(previous_marks))

        # Returns the name of the mark following the given index; if there are no following marks, the method returns an empty string.
        # If the index is in numeric form, the method returns the first mark at that position. If the index is a mark, the method
        # returns the next mark following that mark, which may be at the same numerical position.
        next_marks = self.text.mark_next(1.1)
        cf_logger.info('TextCustom object next_marks {}.'.format(next_marks))

        # Returns a sequence of the names of all the marks in the window, including tk.INSERT and tk.CURRENT.
        names_marks = self.text.mark_names()
        cf_logger.info('TextCustom object names_marks {}.'.format(names_marks))

        # Changes or queries the gravity of an existing mark; see Section 24.2, “Text widget marks”, above, for an explanation of gravity.
        # To set the gravity, pass in the name of the mark, followed by either tk.LEFT or tk.RIGHT. To find the gravity of an existing mark,
        # omit the second argument and the method returns tk.LEFT or tk.RIGHT.
        self.text.mark_gravity('mark#1', tk.LEFT) 

        mark_gravity = self.text.mark_gravity('mark#1')
        cf_logger.info('TextCustom object mark_gravity {}.'.format(mark_gravity))

        # ---------------- tag --------------------------
        # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/text-tag.html

        # This method associates the tag named tagName with a region of the contents starting just after index index1 and extending up
        # to index index2. If you omit index2, only the character after index1 is tagged.
        self.text.tag_add('tag#1', 1.6, 1.9)
        self.text.tag_add('tag#2', 2.0, 2.5)
        self.text.tag_add('tag#3', 3.0, 3.5)

        # To delete one or more tags, pass their names to this method. Their options and bindings go away, and the tags 
        # are removed from all regions of text.
        self.text.tag_delete('tag#2')

        # Removes the tag named tagName from all characters between index1 and index2. If index2 is omitted, the tag 
        # is removed from the single character after index1.
        self.text.tag_remove('tag#3', 3.0, 3.2)

        # This method binds an event to all the text tagged with tagName. See Section 54, “Events”, below, for more information on event bindings.
        # To create a new binding for tagged text, use the first three arguments: sequence identifies the event, and func is the function you want 
        # it to call when that event happens. To add another binding to an existing tag, pass the same first three arguments and '+' as the fourth
        # argument. To find out what bindings exist for a given sequence on a tag, pass only the first two arguments; the method returns the
        # associated function.To find all the bindings for a given tag, pass only the first argument; the method returns a list of all the tag's 
        # sequence arguments.
        self.text.tag_bind('tag#1', '<Button-1>', self.click_callback)

        # Remove the event binding for the given sequence from the tag named tagName. If there are multiple handlers for this sequence and tag, 
        # you can remove only one handler by passing it as the third argument.
        self.text.tag_unbind('tag#1', '<Button-1>')

        """
        To change the value of options for the tag named tagName, pass in one or more option=value pairs.

        If you pass only one argument, you will get back a dictionary defining all the options and their values currently in force for the named tag.

        Here are the options for tag configuration:

        background	The background color for text with this tag. Note that you can't use bg as an abbreviation.

        bgstipple	To make the background appear grayish, set this option to one of the standard bitmap names . 
        This has no effect unless you also specify a background.

        borderwidth	Width of the border around text with this tag. Default is 0. Note that you can't use bd as an abbreviation.

        fgstipple	To make the text appear grayish, set this option a bitmap name.

        font	The font used to display text with this tag. 

        foreground	The color used for text with this tag. Note that you can't use the fg abbreviation here.

        justify	The justify option set on the first character of each line determines how that line is justified: tk.LEFT (the default),
         tk.CENTER, or tk.RIGHT.

        lmargin1	How much to indent the first line of a chunk of text that has this tag. The default is 0. 

        lmargin2	How much to indent successive lines of a chunk of text that has this tag. The default is 0.

        offset	How much to raise (positive values) or lower (negative values) text with this tag relative to the baseline.
        Use this to get superscripts or subscripts, for example. For allowable values, see Section 5.1, “Dimensions”.

        overstrike	Set overstrike=1 to draw a horizontal line through the center of text with this tag.

        relief	Which 3-D effect to use for text with this tag. The default is relief=tk.FLAT; for other possible values see Section 5.6, “Relief styles”.
        rmargin	Size of the right margin for chunks of text with this tag. Default is 0.

        spacing1	This option specifies how much extra vertical space is put above each line of text with this tag. If a line wraps, 
        this space is added only before the first line it occupies on the display. Default is 0.

        spacing2	This option specifies how much extra vertical space to add between displayed lines of text with this tag when a
        logical line wraps. Default is 0.

        spacing3	This option specifies how much extra vertical space is added below each line of text with this tag. 
        If a line wraps, this space is added only after the last line it occupies on the display. Default is 0.

        tabs	How tabs are expanded on lines with this tag. See Section 24.6, “Setting tabs in a Text widget”.

        underline	Set underline=1 to underline text with this tag.

        wrap	How long lines are wrapped in text with this tag. See the description of the wrap option for text widgets, above.

        """
        self.text.tag_config('tag#1', background='red', foreground='blue')

        # Use this method to retrieve the value of the given option for the given tagName.
        tag_bg = self.text.tag_cget('tag#1', 'background')
        cf_logger.info('TextCustom object tag_bg {}.'.format(tag_bg))

        # Use this method to change the order of tags in the tag stack (see Section 24.5, “Text widget tags”, above, for an explanation of the tag stack). 
        # If you pass two arguments, the tag with name tagName is moved to a position just below the tag with name belowThis. If you pass only one argument,
        # that tag is moved to the bottom of the tag stack.
        self.text.tag_lower('tag#1')

        # Use this method to change the order of tags in the tag stack (see Section 24.5, “Text widget tags”, above, for an explanation of the tag stack). 
        # If you pass two arguments, the tag with name tagName is moved to a position just above the tag with name aboveThis. If you pass only one argument,
        # that tag is moved to the top of the tag stack.
        self.text.tag_raise('tag#1')

        # f you pass an index argument, this method returns a sequence of all the tag names that are associated with the character after that index. 
        # If you pass no argument, you get a sequence of all the tag names defined in the text widget.
        tag_names = self.text.tag_names(1.0)
        cf_logger.info('TextCustom object tag_names {}.'.format(tag_names))

        # This method searches a given region for places where a tag named tagName starts. The region searched starts at index index1 and ends 
        # at index index2. If the index2 argument is omitted, the search goes all the way to the end of the text.
        # If there is a place in the given region where that tag starts, the method returns a sequence [i0, i1], where i0 is the index of the 
        # first tagged character and i1 is the index of the position just after the last tagged character.
        # If no tag starts are found in the region, the method returns an empty string.
        tag_position_nextrange = self.text.tag_nextrange('tag#1', 1.0, 1.6)
        cf_logger.info('TextCustom object tag_position_nextrange {}.'.format(tag_position_nextrange))

        # This method searches a given region for places where a tag named tagName starts. The region searched starts before index index1 and ends at index 
        # index2. If the index2 argument is omitted, the search goes all the way to the end of the text.
        # The return values are as in .tag_nextrange().
        tag_position_prevrange = self.text.tag_prevrange('tag#1', 1.6, 1.0)
        cf_logger.info('TextCustom object tag_position_prevrange {}.'.format(tag_position_prevrange))

        # This method finds all the ranges of text in the widget that are tagged with name tagName, and returns a sequence [s0, e0, s1, e1, …], where each 
        # si is the index just before the first character of the range and ei is the index just after the last character of the range.
        tag_position = self.text.tag_ranges('tag#1')
        cf_logger.info('TextCustom object tag_position {}.'.format(tag_position))
        
        # ---------------- window --------------------------

        """
        This method creates a window where a widget can be embedded within a text widget. There are two ways to provide the embedded widget:

        you can use pass the widget to the window option in this method, or

        you can define a procedure that will create the widget and pass that procedure as a callback to the create option.

        Options for .window_create() are:

        align	Specifies how to position the embedded widget vertically in its line, if it isn't as tall as the text on the line. 
        Values include: align=tk.CENTER (the default), which centers the widget vertically within the line; align=tk.TOP, 
        which places the top of the image at the top of the line; align=tk.BOTTOM, which places the bottom of the image at 
        the bottom of the line; and align=tk.BASELINE, which aligns the bottom of the image with the text baseline.

        create	A procedure that will create the embedded widget on demand. This procedure takes no arguments and must create the 
        widget as a child of the text widget and return the widget as its result.

        padx	Extra space added to the left and right of the widget within the text line. Default is 0.

        pady	Extra space added above and below the widget within the text line. Default is 0.

        stretch	This option controls what happens when the line is higher than the embedded widget. Normally this option is
        0, meaning that the embedded widget is left at its natural size. If you set stretch=1, the widget is stretched 
        vertically to fill the height of the line, and the align option is ignored.

        window	The widget to be embedded. This widget must be a child of the text widget.  
        """
        text_child = tk.Text(self.text)
        text_child.config(width=10, height=5)

        self.text.pack()
        self.text.window_create(1.0, window=text_child)

        # To change the value of options for embedded widget at the given index, pass in one or more option=value pairs.
        # If you pass only one argument, you will get back a dictionary defining all the options and their values currently 
        # in force for the given widget.
        self.text.window_configure(1.0, padx=5, pady=5)

        # Returns the value of the given option for the embedded widget at the given index.
        padx_value = self.text.window_cget(1.0, 'padx')
        cf_logger.info('TextCustom object padx_value {}.'.format(padx_value))

        # Returns a sequence containing the names of all embedded widgets.
        widgets = self.text.window_names()
        cf_logger.info('TextCustom object widgets {}.'.format(widgets))

        # ---------------- view --------------------------
        """
        .xview(tk.MOVETO, fraction)
        This method scrolls the text widget horizontally, and is intended for binding to the command option of a related horizontal scrollbar.

        This method can be called in two different ways. The first call positions the text at a value given by fraction, where 0.0 moves the text to its leftmost position and 1.0 to its rightmost position.

        .xview(tk.SCROLL, n, what)
        The second call moves the text left or right: the what argument specifies how much to move and can be either tk.UNITS or tk.PAGES, and n tells how many characters or pages to move the text to the right relative to its image (or left, if negative).

        .xview_moveto(fraction)
        This method scrolls the text in the same way as .xview(tk.MOVETO, fraction).

        .xview_scroll(n, what)
        Same as .xview(tk.SCROLL, n, what).

        .yview(tk.MOVETO, fraction)
        The vertical scrolling equivalent of .xview(tk.MOVETO,…).

        .yview(tk.SCROLL, n, what)
        The vertical scrolling equivalent of .xview(tk.SCROLL,…). When scrolling vertically by tk.UNITS, the units are lines.

        .yview_moveto(fraction)
        The vertical scrolling equivalent of .xview_moveto().

        .yview_scroll(n, what)
        The vertical scrolling equivalent of .xview_scroll().
        """
        # refer to listbox object about the xview, yview
        # refer to entry object about the scroll, moveto views

    def click_callback(self, event):
        cf_logger.info('tag callback.')


class ScrolledTextCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init scrolltext
        pass

        # from tkinter.scrolledtext import ScrolledText
        # refer to sample in tkinter.scrolledtext


class BitmapImageCustom(object):
    def __init__(self, master=None):

        # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/bitmaps.html
        # bitmaps - The graphic above shows Button widgets bearing the standard bitmaps. From left to right, 
        # they are 'error', 'gray75', 'gray50', 'gray25', 'gray12', 'hourglass', 'info', 'questhead', 
        # 'question', and 'warning'.

        # init BitmapImage
        self.bmImage = tk.BitmapImage('error')
        
        # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/images.html
        # tk.BitmapImage(file=f[, background=b][, foreground=c])


class PhotoImageCustom(object):
    def __init__(self, master=None):

        # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/images.html
        #  tk.PhotoImage(file=f)

        # init PhotoImage
        self.photoImage = tk.PhotoImage(file='') # file is a image
        
        # methods of PhotoImage object

        #    def blank(self):
        #    """Display a transparent image."""
        
        #    def cget(self, option):
        #        """Return the value of OPTION."""

         
        #    def copy(self):
        #        """Return a new PhotoImage with the same image as this widget."""
            
        #    def zoom(self, x, y=''):
        #        """Return a new PhotoImage with the same image as this widget
        #        but zoom it with a factor of x in the X direction and y in the Y
        #        direction.  If y is not given, the default value is the same as x.
        #        """
                
        #    def subsample(self, x, y=''):
        #        """Return a new PhotoImage based on the same image as this widget
        #        but use only every Xth or Yth pixel.  If y is not given, the
        #        default value is the same as x.
        #        """
                
        #    def get(self, x, y):
        #        """Return the color (red, green, blue) of the pixel at X,Y."""
                
        #    def put(self, data, to=None):
        #       """Put row formatted colors to image starting from
        #        position TO, e.g. image.put("{red green} {blue yellow}", to=(4,6))"""
                
        #    # XXX read
        #    def write(self, filename, format=None, from_coords=None):
        #        """Write image to file FILENAME in FORMAT starting from
        #        position FROM_COORDS."""
            

class CanvasCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init canvas
        self.canvas = tk.Canvas(self.master)
        self.canvas.config(width=200, height=200)

        self.canvas.pack()

    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.canvas.config(bg='red'), self.canvas.config(background='red'),
        # configure -> self.canvas.configure(bg='red'), self.canvas.configure(background='red'),
        # self.canvas['bg'] = 'red', self.canvas['background'] = 'red'

        # Valid resource names:
        # background, bd, bg, borderwidth, closeenough,
        # confine, cursor, height, highlightbackground, highlightcolor,
        # highlightthickness, insertbackground, insertborderwidth,
        # insertofftime, insertontime, insertwidth, offset, relief,
        # scrollregion, selectbackground, selectborderwidth, selectforeground,
        # state, takefocus, width, xscrollcommand, xscrollincrement,
        # yscrollcommand, yscrollincrement.

        self.canvas[key] = value

    def resource_set_test(self):
        """
        Set the widget resource name with different value
        Try 
        closeenough, confine, scrollregion, xscrollcommand, xscrollincrement,
        yscrollcommand, yscrollincrement

        Not sure: offset

        Other resources refer to basic_widgets.py
        """
        # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/canvas.html

        # resource name: closeenough
        # note: A float that specifies how close the mouse must be to an item to be considered inside it. Default is 1.0.
        # resource value: float
        self.resource_set('closeenough', 1.0)
        cf_logger.info('CanvasCustom object set closeenough.')

        # resource name: confine
        # note: If true (the default), the canvas cannot be scrolled outside of the scrollregion (see below).
        # resource value: True, False
        self.resource_set('confine', False)
        cf_logger.info('CanvasCustom object set confine.')

        # resource name: scrollregion
        # note: A tuple (w, n, e, s) that defines over how large an area the canvas can be scrolled, where w is the left 
        # side, n the top, e the right side, and s the bottom.
        # resource value: A tuple (w, n, e, s)

        # resource name: xscrollcommand, xscrollincrement, yscrollcommand, yscrollincrement
        # note: If the canvas is scrollable, set this option to the .set() method of the scrollbar.
        # increment is the step when scrolling
        # refer to Entry and Text object
    
    def methods_canvas_test(self):
        # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/canvas-methods.html
        # methods includes operate widgets, operate tags, operate resouces, operate interaction...

    def methods_create_childwidgets_test(self):
        # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/canvas.html
        # create different child widgets on canvas
        


if __name__ == '__main__':
    # go to test.py
    pass 

